package lore.soft.fullkeyboard;

import android.app.Dialog;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.os.IBinder;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import lore.soft.fullkeyboard.utils.KeyEventAscii;
import lore.soft.fullkeyboard.utils.MacroKey;

/**
 * Example of writing an input method for a soft keyboard.  This code is
 * focused on simplicity over completeness, so it should in no way be considered
 * to be a complete soft keyboard implementation.  Its purpose is to provide
 * a basic example for how you would get started writing an input method, to
 * be fleshed out as appropriate.
 */
public class SoftKeyboard extends InputMethodService 
        implements KeyboardView.OnKeyboardActionListener {
    static final boolean DEBUG = false;
    
    /**
     * This boolean indicates the optional example code for performing
     * processing of hard keys in addition to regular text generation
     * from on-screen interaction.  It would be used for input methods that
     * perform language translations (such as converting text entered on 
     * a QWERTY keyboard to Chinese), but may not be used for input methods
     * that are primarily intended to be used for on-screen text entry.
     */
    private static final boolean PROCESS_HARD_KEYS = true;

    private InputMethodManager mInputMethodManager;

    private FullKeyboardView mInputView;

    private int mLastDisplayWidth;
    private long mMetaState;
    
    private FullKeyboard mQwertyKeyboard;
    private FullKeyboard mQwertyShiftedKeyboard;
    private FullKeyboard mQwertyFuncKeyboard;
    private FullKeyboard mQwertyShiftedFuncKeyboard;
    private FullKeyboard mSymbolKeyboard;

    private boolean mModCtrl = false;
    private boolean mModAlt = false;
    private boolean mModMeta = false;

    private FullKeyboard mCurrKeyboard;

    private MacroKey[] macroKey = {
            new MacroKey("ESC", KeyEvent.KEYCODE_ESCAPE),
            new MacroKey("C-]", KeyEvent.KEYCODE_CTRL_LEFT,
                    KeyEvent.KEYCODE_LEFT_BRACKET),
            new MacroKey("C-t", KeyEvent.KEYCODE_CTRL_LEFT,KeyEvent.KEYCODE_T),
            new MacroKey("C-i", KeyEvent.KEYCODE_CTRL_LEFT,KeyEvent.KEYCODE_I),
            new MacroKey("C-o", KeyEvent.KEYCODE_CTRL_LEFT,KeyEvent.KEYCODE_O),
            new MacroKey("#", KeyEvent.KEYCODE_POUND),
            new MacroKey("*", KeyEvent.KEYCODE_STAR),
            new MacroKey("%", KeyEvent.KEYCODE_SHIFT_LEFT,KeyEvent.KEYCODE_5),
            new MacroKey("n", KeyEvent.KEYCODE_N),
            new MacroKey("tl", KeyEvent.KEYCODE_SHIFT_LEFT,
                    KeyEvent.KEYCODE_SEMICOLON,
                    KeyEvent.KEYCODE_T,
                    KeyEvent.KEYCODE_SHIFT_LEFT,
                    KeyEvent.KEYCODE_L,
                    KeyEvent.KEYCODE_I,
                    KeyEvent.KEYCODE_S,
                    KeyEvent.KEYCODE_T,
                    KeyEvent.KEYCODE_ENTER)
    };

    /**
     * Main initialization of the input method component.  Be sure to call
     * to super class.
     */
    @Override public void onCreate() {
        super.onCreate();
        mInputMethodManager = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
    }
    
    /**
     * This is the point where you can do all of your UI initialization.  It
     * is called after creation and any configuration change.
     */
    @Override public void onInitializeInterface() {
        if (mQwertyKeyboard != null) {
            // Configuration changes can happen after the keyboard gets recreated,
            // so we need to be able to re-build the keyboards if the available
            // space has changed.
            int displayWidth = getMaxWidth();
            if (displayWidth == mLastDisplayWidth) return;
            mLastDisplayWidth = displayWidth;
        }
        mQwertyKeyboard = new FullKeyboard(this, R.xml.qwerty);
        mQwertyShiftedKeyboard = new FullKeyboard(this, R.xml.qwerty_shift);
        mQwertyShiftedKeyboard.setShifted(true);
//        mQwertyFuncKeyboard = new FullKeyboard(this, R.xml.qwerty_function);
//        mQwertyShiftedFuncKeyboard = new FullKeyboard(this, R.xml.qwerty_shift_function);

        mSymbolKeyboard = new FullKeyboard(this, R.xml.symbols);
        int index= 0;
        for (MacroKey l : macroKey) {
            mSymbolKeyboard.setCustomKey(index++, l.mLabel);
        }
    }
    
    /**
     * Called by the framework when your view for creating input needs to
     * be generated.  This will be called the first time your input method
     * is displayed, and every time it needs to be re-created such as due to
     * a configuration change.
     */
    @Override public View onCreateInputView() {
        mInputView = (FullKeyboardView) getLayoutInflater().inflate(
                R.layout.input, null);
        mInputView.setOnKeyboardActionListener(this);
        setKeyboard(mQwertyKeyboard);
        return mInputView;
    }

    private void setKeyboard(FullKeyboard nextKeyboard) {
        mInputView.setKeyboard(nextKeyboard);
        mCurrKeyboard= nextKeyboard;
    }

    /**
     * This is the main point where we do our initialization of the input method
     * to begin operating on an application.  At this point we have been
     * bound to the client, and are now receiving all of the detailed information
     * about the target of our edits.
     */
    @Override public void onStartInput(EditorInfo attribute, boolean restarting) {
        super.onStartInput(attribute, restarting);

        if (!restarting) {
            // Clear shift states.
            mMetaState = 0;
        }
        // We are now going to initialize our state based on the type of
        // text being edited.
        switch (attribute.inputType & InputType.TYPE_MASK_CLASS) {
            case InputType.TYPE_CLASS_NUMBER:
            case InputType.TYPE_CLASS_DATETIME:
            case InputType.TYPE_CLASS_PHONE:
            case InputType.TYPE_CLASS_TEXT:
            default:
                // For all unknown input types, default to the alphabetic
                // keyboard with no special features.
                mCurrKeyboard= mQwertyKeyboard;
                clearMetaKeyState();
        }

    }

    /**
     * This is called when the user is done editing a field.  We can use
     * this to reset our state.
     */
    @Override public void onFinishInput() {
        super.onFinishInput();
        if (mInputView != null) {
            mInputView.closing();
        }
    }
    
    @Override public void onStartInputView(EditorInfo attribute, boolean restarting) {
        super.onStartInputView(attribute, restarting);
        // Apply the selected keyboard to the input view.
        setKeyboard(mCurrKeyboard);
        mInputView.closing();
    }

    /**
     * Use this to monitor key events being delivered to the application.
     * We get first crack at them, and can either resume them or let them
     * continue to the app.
     */
    @Override public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_BACK:
                // The InputMethodService already takes care of the back
                // key for us, to dismiss the input method if it is shown.
                // However, our keyboard could be showing a pop-up window
                // that back should dismiss, so we first allow it to do that.
                if (event.getRepeatCount() == 0 && mInputView != null) {
                    if (mInputView.handleBack()) {
                        return true;
                    }
                }
                break;
                
            case KeyEvent.KEYCODE_DEL:
                // Special handling of the delete key: if we currently are
                // composing text for the user, we want to modify that instead
                // of let the application to the delete itself.
                break;
                
            case KeyEvent.KEYCODE_ENTER:
                // Let the underlying text editor always handle these.
                return false;
                
            default:
                // For all other keys, if we want to do transformations on
                // text being entered with a hard keyboard, we need to process
                // it and do the appropriate action.
                if (PROCESS_HARD_KEYS) {
                    if (keyCode == KeyEvent.KEYCODE_SPACE
                            && (event.getMetaState()&KeyEvent.META_ALT_ON) != 0) {
                        // A silly example: in our input method, Alt+Space
                        // is a shortcut for 'android' in lower case.
                        InputConnection ic = getCurrentInputConnection();
                        if (ic != null) {
                            // First, tell the editor that it is no longer in the
                            // shift state, since we are consuming this.
                            ic.clearMetaKeyStates(KeyEvent.META_ALT_ON);
                            keyDownUp(KeyEvent.KEYCODE_A);
                            keyDownUp(KeyEvent.KEYCODE_N);
                            keyDownUp(KeyEvent.KEYCODE_D);
                            keyDownUp(KeyEvent.KEYCODE_R);
                            keyDownUp(KeyEvent.KEYCODE_O);
                            keyDownUp(KeyEvent.KEYCODE_I);
                            keyDownUp(KeyEvent.KEYCODE_D);
                            // And we consume this event.
                            return true;
                        }
                    }
                }
        }
        
        return super.onKeyDown(keyCode, event);
    }

    /**
     * Use this to monitor key events being delivered to the application.
     * We get first crack at them, and can either resume them or let them
     * continue to the app.
     */
    @Override public boolean onKeyUp(int keyCode, KeyEvent event) {
        // If we want to do transformations on text being entered with a hard
        // keyboard, we need to process the up events to update the meta key
        // state we are tracking.

        return super.onKeyUp(keyCode, event);
    }

    /**
     * Helper to send a key down / key up pair to the current editor.
     */
    private void keyDownUp(int keyEventCode) {
        getCurrentInputConnection().sendKeyEvent(
                new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode));
        getCurrentInputConnection().sendKeyEvent(
                new KeyEvent(KeyEvent.ACTION_UP, keyEventCode));
    }


    // Implementation of KeyboardViewListener

    public void onKey(int primaryCode, int[] keyCodes) {
        if (primaryCode == Keyboard.KEYCODE_CANCEL) {
            handleClose();
        } else if (primaryCode == getResources().getInteger(R.integer.KEYCODE_OPTIONS)) {
            handleLanguageSwitch();
        } else if (primaryCode == getResources().getInteger(R.integer.KEYCODE_TEST_0)) {
            KeyEventAscii key = new KeyEventAscii('@');
            key.sendKeyDown(getCurrentInputConnection(),
                    true, mModCtrl, mModAlt, false);
            key.sendKeyUp(getCurrentInputConnection(),
                    true, mModCtrl, mModAlt, false);
        } else if (primaryCode <= getResources().getInteger(R.integer.KEYCODE_CUSTOM_START)
                && primaryCode >= getResources().getInteger(R.integer.KEYCODE_CUSTOM_END)) {
            sendMacro(getResources().getInteger(R.integer.KEYCODE_CUSTOM_START)- primaryCode);
        } else {
            if (primaryCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
                    primaryCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
                handleShift();
            } else if (primaryCode == KeyEvent.KEYCODE_FUNCTION) {
                handleModeSwitch();
            } else if (primaryCode == KeyEvent.KEYCODE_ALT_LEFT) {
                mModAlt = mCurrKeyboard.getAltKey().on;
            } else if (primaryCode == KeyEvent.KEYCODE_CTRL_LEFT) {
                mModCtrl = mCurrKeyboard.getCtrlKey().on;
            } else {
                sendKeyEventDownUp(getCurrentInputConnection(), primaryCode,
                        getMetaState(mCurrKeyboard.isShifted(), mModCtrl, mModAlt, false));
                clearMetaKeyState();
            }
        }
    }

    @Override
    public void onText(CharSequence text) {

    }

    private int getMetaState(
            boolean shifted, boolean ctrled, boolean alted, boolean metaed) {
        int meta = 0;
        if (shifted) meta |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
        if (ctrled) meta |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
        if (alted) meta |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
        if (metaed) meta |= KeyEvent.META_META_ON | KeyEvent.META_META_LEFT_ON;
        return meta;
    }

    private void sendKeyEvent(InputConnection ic, int code, boolean upDown, int meta) {
        long now = System.currentTimeMillis();
        if (ic != null) ic.sendKeyEvent(new KeyEvent(now, now,
                upDown? KeyEvent.ACTION_UP: KeyEvent.ACTION_DOWN,
                code, 0, meta));
    }

    private void sendKeyEventDownUp(InputConnection ic, int code, int meta) {
        sendKeyEvent(ic, code, false, meta);
        sendKeyEvent(ic, code, true, meta);
    }

    private void sendMacro(int index) {
        boolean shifted= false;
        boolean ctrled= false;
        boolean alted= false;
        boolean metaed= false;

        for (int m : macroKey[index].mMacroList) {
            if (KeyEvent.KEYCODE_SHIFT_LEFT == m) {
                shifted= !shifted;
            } else if (KeyEvent.KEYCODE_CTRL_LEFT == m) {
                ctrled= !ctrled;
            } else if (KeyEvent.KEYCODE_ALT_LEFT == m) {
                alted= !alted;
            } else if (KeyEvent.KEYCODE_META_LEFT == m) {
                metaed = !metaed;
            } else {
                sendKeyEventDownUp(getCurrentInputConnection(), m,
                        getMetaState(shifted, ctrled, alted, metaed));
            }
        }
    }

    private void clearMetaKeyState() {
        if (mCurrKeyboard == mQwertyShiftedKeyboard &&
                !mQwertyShiftedKeyboard.isShiftLocked()) {
            setKeyboard(mQwertyKeyboard);
        }else if (mCurrKeyboard == mQwertyShiftedFuncKeyboard &&
                !mQwertyShiftedFuncKeyboard.isShiftLocked()) {
            setKeyboard(mQwertyFuncKeyboard);
        }
        if (mModCtrl) {
            mModCtrl= false;
            mCurrKeyboard.getCtrlKey().on = mModCtrl;
            mInputView.invalidateAllKeys();
        }
        if (mModAlt) {
            mModAlt= false;
            mCurrKeyboard.getAltKey().on = mModAlt;
            mInputView.invalidateAllKeys();
        }
    }

    private void handleShift() {
        if (mInputView == null) {
            return;
        }

        if (mCurrKeyboard == mQwertyKeyboard) {
            mQwertyKeyboard.setShiftLock(false);
            mQwertyShiftedKeyboard.setShiftLock(false);
            setKeyboard(mQwertyShiftedKeyboard);
        } else if (mCurrKeyboard == mQwertyShiftedKeyboard) {
            if (mQwertyShiftedKeyboard.isShiftLocked()) {
                mQwertyShiftedKeyboard.setShiftLock(false);
                setKeyboard(mQwertyKeyboard);
            } else {
                mQwertyShiftedKeyboard.setShiftLock(true);
                mInputView.invalidateAllKeys();
            }
        } else if (mCurrKeyboard == mQwertyFuncKeyboard) {
            mQwertyFuncKeyboard.setShiftLock(false);
            setKeyboard(mQwertyShiftedFuncKeyboard);
        } else if (mCurrKeyboard == mQwertyShiftedFuncKeyboard) {
            if (mQwertyShiftedFuncKeyboard.isShiftLocked()) {
                mQwertyShiftedFuncKeyboard.setShiftLock(false);
                setKeyboard(mQwertyFuncKeyboard);
            } else {
                mQwertyShiftedFuncKeyboard.setShiftLock(true);
                mInputView.invalidateAllKeys();
            }
        }
    }

    private void handleModeSwitch() {
        if (mInputView == null) {
            return;
        }
        if(mCurrKeyboard != mSymbolKeyboard) {
            setKeyboard(mSymbolKeyboard);
        }
    }

    private void handleClose() {
        requestHideSelf(0);
        mInputView.closing();
    }

    private IBinder getToken() {
        final Dialog dialog = getWindow();
        if (dialog == null) {
            return null;
        }
        final Window window = dialog.getWindow();
        if (window == null) {
            return null;
        }
        return window.getAttributes().token;
    }

    private void handleLanguageSwitch() {
        mInputMethodManager.switchToNextInputMethod(getToken(), false /* onlyCurrentIme */);
    }

    public void swipeRight() {
        if(mCurrKeyboard == mSymbolKeyboard) {
            setKeyboard(mQwertyKeyboard);
        }
    }
    
    public void swipeLeft() {
        if(mCurrKeyboard == mSymbolKeyboard) {
            setKeyboard(mQwertyKeyboard);
        }
    }

    public void swipeDown() {
        handleClose();
    }

    public void swipeUp() {}
    
    public void onPress(int primaryCode) {}
    
    public void onRelease(int primaryCode) {}
}
